package org.geekbang.projects.giteeoauth2;

import org.springframework.boot.json.JacksonJsonParser;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;

/**
 * Gitee码云OAuth2认证SecurityConfigurer
 */
public class GiteeOAuth2LoginConfigurer<H extends HttpSecurityBuilder<H>> extends
        AbstractHttpConfigurer<GiteeOAuth2LoginConfigurer<H>, H> {

    // 1、创建接入码云的应用,并把 CLIENT_ID 与 CLIENT_SECRET 赋值给下面的静态成员变量
    static final String CLIENT_ID = "1f47599df15a5107d88b4e105d86879797a22f9c5646f069973d8f0673dd0e00";
    static final String CLIENT_SECRET = "eccfa8a6ea24a27c744e799939e75326ddb7fba00f0c546fb7544dae9be1ad62";

    /**
     * 自定义安全过滤链
     */
    @Override
    public void configure(H http) {
        http.addFilterAfter(new GiteeOAuth2RedirectFilter(), LogoutFilter.class);
        http.addFilterAfter(new GiteeOAuth2LoginAuthenticationFilter(),LogoutFilter.class);
    }

    /**
     * 2、重定向过滤器
     */
    static class GiteeOAuth2RedirectFilter extends OncePerRequestFilter {
        private static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/gitee";

        private static final String REDIRECT_URI = "http://localhost:8080/login/oauth2/code/gitee";

        private static final String GITEE_REDIRECT_URI = "https://gitee.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code";

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            if (!request.getRequestURI().endsWith(DEFAULT_AUTHORIZATION_REQUEST_BASE_URI)) {
                filterChain.doFilter(request, response);
                return;
            }
            String uri = UriComponentsBuilder.
                    fromUriString(GITEE_REDIRECT_URI).encode()
                    .buildAndExpand(CLIENT_ID,REDIRECT_URI).toUriString();
            response.sendRedirect(uri);
        }
    }

    /**
     * 码云OAuth2认证过滤器
     */
    static class GiteeOAuth2LoginAuthenticationFilter extends OncePerRequestFilter {
        private static final String DEFAULT_CALLBACK_BASE_URI = "/login/oauth2/code/gitee";
        private static final String DEFAULT_LOGIN_SUCCESS_REDIRECT_URL = "/user";
        private static final ProviderManager providerManager = new ProviderManager(Collections.singletonList(new GiteeOAuth2AuthenticationProvider()));

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            // 检查请求是否‘/login/oauth2/code/gitee’和 是否有code参数。
            String code = request.getParameter("code");
            if (!request.getRequestURI().endsWith(DEFAULT_CALLBACK_BASE_URI) || !StringUtils.hasText(code)) {
                filterChain.doFilter(request, response);
                return;
            }

            // Spring Security基本校验流程
            GiteeOAuth2LoginAuthenticationToken giteeOAuth2LoginAuthenticationToken = new GiteeOAuth2LoginAuthenticationToken(code, request);
            Authentication successAuthentication;
            try {
                successAuthentication = providerManager.authenticate(giteeOAuth2LoginAuthenticationToken);
            } catch (AuthenticationException e) {
                return;
            }

            SecurityContextHolder.getContext().setAuthentication(successAuthentication);
            request.changeSessionId();
            request.getSession().removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
            redirectStrategy(request,response);
        }

        /**
         * Gitee 登录成功的重定向策略
         */
        private void redirectStrategy(HttpServletRequest request, HttpServletResponse response) throws IOException {
            String redirectUrl = DEFAULT_LOGIN_SUCCESS_REDIRECT_URL;
            new DefaultRedirectStrategy().sendRedirect(request, response, redirectUrl);
        }
    }

    /**
     * 自定义的AuthenticationProvider
     * 通过code获取access_token,再由access_token拉取码云授权用户的信息
     */
    static class GiteeOAuth2AuthenticationProvider implements AuthenticationProvider {

        private static final String REDIRECT_URI = "http://localhost:8080/login/oauth2/code/gitee";
        private static final String ACCESS_TOKEN_API_URI = "https://gitee.com/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}&redirect_uri={redirect_uri}&client_secret={client_secret}";
        private static final String USER_INFO_URI = "https://gitee.com/api/v5/user?access_token={access_token}";
        private static final RestTemplate rest = new RestTemplate();

        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            // 用户认证前，构造一个GiteeOAuth2LoginAuthenticationToken
            GiteeOAuth2LoginAuthenticationToken authenticationToken = (GiteeOAuth2LoginAuthenticationToken) authentication;
            // 通过码云API获取access_token
            String accessToken = getAccessToken(authenticationToken.getCode());
            // 通过码云API获取Gitee授权用户的资料
            Map<String, Object> userInfo = getUserInfo(accessToken);
            // 认证成功后，重新生成Authentication
            return createSuccessAuthentication(Objects.requireNonNull(userInfo), authenticationToken.getRequest());
        }

        /**
         * 3、获取码云API的访问令牌access_token
         */
        private String getAccessToken(String code) {
            URI uri = UriComponentsBuilder
                    .fromUriString(ACCESS_TOKEN_API_URI).encode()
                    .buildAndExpand(code,CLIENT_ID,REDIRECT_URI,CLIENT_SECRET).toUri();
            RequestEntity<Void> requestEntity = RequestEntity.post(uri).
                    header("User-Agent","Mozilla/5.0").build();
            ResponseEntity<String> exchange = rest.exchange(requestEntity, String.class);
            String resultJson = exchange.getBody();
            Map<String, Object> stringObjectMap = new JacksonJsonParser().parseMap(resultJson);
            return (String)stringObjectMap.get("access_token");
        }

        /**
         * 4、获取码云授权用户的信息
         */
        private Map<String, Object> getUserInfo(String accessToken) {
            URI uri = UriComponentsBuilder
                    .fromUriString(USER_INFO_URI).buildAndExpand(accessToken).toUri();
            RequestEntity<Void> requestEntity = RequestEntity.get(uri).
                    header("User-Agent","Mozilla/5.0").build();
            ResponseEntity<String> response = rest.exchange(requestEntity,String.class);
            String body = response.getBody();
            return new JacksonJsonParser().parseMap(body);
        }

        /**
         * 认证成功后，重新构造Authentication。
         */
        private Authentication createSuccessAuthentication(Map<String, Object> userInfo, HttpServletRequest request) {
            GiteeUserInfoDetail user = new GiteeUserInfoDetail(userInfo);
            GiteeOAuth2LoginAuthenticationToken authenticationToken = new GiteeOAuth2LoginAuthenticationToken(user, AuthorityUtils.createAuthorityList("USER"));
            AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
            authenticationToken.setDetails(authenticationDetailsSource.buildDetails(request));
            return authenticationToken;
        }

        @Override
        public boolean supports(Class<?> authentication) {
            return GiteeOAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication);
        }
    }

    /**
     * 自定义的Authentication
     * <p></p>
     * 校验前只包含code与reuqest;
     * <p>
     * 校验成功后包含principal、authorities.
     */
    static class GiteeOAuth2LoginAuthenticationToken extends AbstractAuthenticationToken {

        private String code;
        private Object principal;
        private HttpServletRequest request;

        public GiteeOAuth2LoginAuthenticationToken(String code, HttpServletRequest request) {
            super(Collections.emptyList());
            this.code = code;
            this.request = request;
        }

        public GiteeOAuth2LoginAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principal = principal;
            setAuthenticated(true);
        }

        public String getCode() {
            return code;
        }

        public HttpServletRequest getRequest() {
            return request;
        }

        @Override
        public Object getCredentials() {
            return "";
        }

        @Override
        public Object getPrincipal() {
            return this.principal;
        }
    }
}
